1 module hip.util.path; 2 import hip.util.string; 3 import hip.util.system; 4 //Node required for buildFolderTree 5 public import hip.util.data_structures: Node; 6 7 version(Windows) 8 enum defaultCaseSensitivity = false; 9 else version(Darwin) 10 enum defaultCaseSensitivity = false; 11 else// version(Posix) 12 enum defaultCaseSensitivity = true; 13 14 version(Windows) 15 { 16 enum pathSeparator = '\\'; 17 enum otherSeparator = '/'; 18 } 19 else 20 { 21 enum pathSeparator = '/'; 22 enum otherSeparator = '\\'; 23 } 24 25 string[] pathSplitter(string path) @safe pure nothrow 26 { 27 string[] ret; 28 foreach(p; pathSplitterRange(path)) 29 ret~= p; 30 return ret; 31 } 32 33 auto pathSplitterRange(string path) pure @safe nothrow @nogc 34 { 35 struct PathRange 36 { 37 string path; 38 size_t indexRight = 0; 39 40 bool empty() @safe pure nothrow @nogc {return indexRight >= path.length;} 41 string front() @safe pure nothrow @nogc 42 { 43 size_t i = indexRight; 44 while(i < path.length && path[i] != '\\' && path[i] != '/') 45 i++; 46 indexRight = i; 47 return path[0..indexRight]; 48 } 49 void popFront() @safe pure nothrow @nogc 50 { 51 if(indexRight+1 < path.length) 52 { 53 path = path[indexRight+1..$]; 54 indexRight = 0; 55 } 56 else 57 indexRight+= 1; //Guarantees empty 58 } 59 } 60 61 return PathRange(path); 62 } 63 64 bool isRootOf(string theRoot, string ofWhat) pure nothrow @nogc 65 { 66 auto pathA = pathSplitterRange(theRoot); 67 auto pathB = pathSplitterRange(ofWhat); 68 69 for(; !pathA.empty && !pathB.empty; pathA.popFront, pathB.popFront) 70 { 71 string compA = pathA.front; 72 string compB = pathB.front; 73 if(compA != compB) 74 return false; 75 } 76 return true; 77 } 78 79 80 81 string relativePath(bool caseSensitive = defaultCaseSensitivity)(string filePath, string base) pure nothrow @safe 82 { 83 int commonIndex = 0; 84 bool isEqual = true; 85 for(int i = 0; i < base.length; i++) 86 { 87 if(i == filePath.length || (caseSensitive ? base[i] != filePath[i] : base[i].toLowerCase != filePath[i].toLowerCase)) 88 { 89 isEqual = false; 90 break; 91 } 92 else if(base[i] == pathSeparator) 93 commonIndex = cast(int)i; 94 } 95 if(isEqual) 96 { 97 if(filePath.length == base.length) 98 return "."; 99 else //If the base string is a subset, return part after base. 100 return filePath[base.length + (filePath[base.length] == pathSeparator ? 1 : 0)..$]; 101 } 102 else if(commonIndex == 0) 103 return filePath; 104 105 string ret; 106 for(uint i = commonIndex; i < base.length; i++) 107 { 108 if(base[i] == pathSeparator) 109 ret~= ".."~pathSeparator; 110 } 111 ret~= filePath[commonIndex] == pathSeparator ? filePath[commonIndex+1..$] : filePath[commonIndex..$]; 112 return ret; 113 } 114 115 bool isAbsolutePath(string fPath) pure nothrow @nogc @safe 116 { 117 if(fPath == null) 118 return false; 119 version(Posix) 120 if(fPath[0] != '/') 121 return false; 122 version(Windows) 123 { 124 if(fPath.length < 3) 125 return false; 126 if(!(fPath[0].isUpperCase && fPath[1] == ':' && fPath[2] == '\\')) 127 return false; 128 } 129 for(size_t i = 0; i < fPath.length; i++) 130 if(i + 2 < fPath.length && fPath[i] == '.' && fPath[i+1] == '.' && fPath[i+2] == pathSeparator) 131 return false; 132 return true; 133 } 134 135 string absolutePath(string thePath, string currPath) 136 { 137 if(isAbsolutePath(thePath)) return thePath; 138 return joinPath(currPath, thePath); 139 } 140 141 142 143 char determineSeparator(const string filePath) pure nothrow @nogc @safe 144 { 145 size_t i = 0; 146 while(i < filePath.length && filePath[i] != '/' && filePath[i] != '\\') 147 i++; 148 return i < filePath.length ? filePath[i] : '\0'; 149 } 150 151 ///Will get the directory name until a trailing separator or return 152 string dirName(string filePath) pure nothrow @nogc @safe 153 { 154 char sep = determineSeparator(filePath); 155 if(sep == '\0') 156 return "."; 157 int last = filePath.lastIndexOf(sep); 158 if(last == -1) 159 return "."; 160 return filePath[0..last]; 161 } 162 163 164 string filename(string filePath) @safe pure nothrow @nogc 165 { 166 char sep = determineSeparator(filePath); 167 if(sep == '\0') 168 return filePath; 169 int last = filePath.lastIndexOf(sep); 170 if(last == -1) 171 return filePath; 172 return filePath[last+1..$]; 173 } 174 175 alias baseName = filename; 176 177 ref string filename(return ref string filePath, string newFileName) @safe pure nothrow 178 { 179 return filePath = replaceFileName(filePath, newFileName); 180 } 181 182 string filenameNoExt(string filePath) @safe pure nothrow @nogc 183 { 184 string f = filePath.filename; 185 if(f == "") 186 return ""; 187 int last = f.lastIndexOf("."); 188 if(last == -1) 189 return f; 190 return f[0..last]; 191 } 192 193 string replaceFileName(string filePath, string newFileName) @safe pure nothrow 194 { 195 char sep = determineSeparator(filePath); 196 string[] p = pathSplitter(filePath); 197 p[$-1] = newFileName; 198 return ((p[0] == "" && sep == '/') ? "/" : "") ~ joinPath(sep, p); 199 } 200 201 string normalizePath(string path) 202 { 203 string[16] normalized; 204 size_t pathsLength; 205 foreach(p; pathSplitterRange(path)) 206 { 207 if(p == ".") 208 continue; 209 else if(p == "..") 210 { 211 if(pathsLength > 0) 212 pathsLength--; 213 else 214 normalized[pathsLength++]= p; 215 } 216 else 217 normalized[pathsLength++]= p; 218 219 } 220 return normalized[0..pathsLength].joinPath; 221 } 222 223 224 225 /** 226 * Extension getter 227 ```d 228 string myFile = "test.png"; 229 writeln(myFile.extension); //png 230 ``` 231 */ 232 string extension(string pathOrFilename) pure nothrow @nogc @safe 233 { 234 auto ind = pathOrFilename.lastIndexOf("."); 235 if(ind == -1) 236 return ""; 237 return pathOrFilename[cast(uint)ind+1..$]; 238 } 239 240 /** 241 * Extension setter. 242 * Usage: 243 ```d 244 string test = "test.png" 245 test.extension = "txt"; 246 writeln(test); //test.txt 247 ``` 248 */ 249 ref string extension(return ref string pathOrFilename, string newExt) 250 { 251 auto ind = pathOrFilename.lastIndexOf("."); 252 if(ind != -1 && ind != pathOrFilename.length) 253 { 254 if(newExt.length == 0) 255 pathOrFilename = pathOrFilename[0..ind]; 256 else if(newExt[0] != '.') 257 pathOrFilename = pathOrFilename[0..ind+1]~newExt; 258 else 259 pathOrFilename = pathOrFilename[0..ind+1]~newExt[1..$]; 260 } 261 return pathOrFilename; 262 } 263 264 string extension(string pathOrFilename, string newExt) 265 { 266 pathOrFilename = pathOrFilename.extension(newExt); 267 return pathOrFilename; 268 } 269 270 string joinPath(char separator, scope const string[] paths ...) @safe pure nothrow 271 { 272 if(paths.length == 1) 273 return paths[0]; 274 275 PathString output; 276 for(int i = 0; i < paths.length; i++) 277 { 278 string next = i+1 < paths.length ? paths[i+1] : ""; 279 if(paths[i] != "") 280 { 281 output~=paths[i]; 282 if(next != "" && next[0] != separator && 283 paths[i][$-1] != separator) 284 output~=separator; 285 } 286 else 287 { 288 if(next != "" && next[0] != separator) 289 output~= separator; 290 } 291 } 292 return output.toString.dup; 293 } 294 295 string joinPath(scope const string[] paths ...) @safe pure nothrow 296 { 297 char sep; 298 foreach(p; paths) 299 { 300 sep = determineSeparator(p); 301 if(sep != '\0') 302 break; 303 } 304 if(sep == '\0') 305 sep = pathSeparator; 306 return joinPath(sep, paths); 307 } 308 309 310 public Node!string buildFolderTree(string[] filesList) 311 { 312 alias DirNode = Node!string; 313 DirNode root = new DirNode(filesList[0]); 314 315 scope DirNode[] dirStack = [root]; 316 317 for(size_t i = 1; i < filesList.length; i++) 318 { 319 int currStack = 0; 320 foreach(pathPart; pathSplitterRange(filesList[i])) 321 { 322 if(pathPart.extension != "") //It is a leaf if it has an extension 323 { 324 dirStack[$-1].addChild(pathPart); 325 } 326 else if(currStack >= dirStack.length) //If we have more parts than the stack has children, add to the stack 327 { 328 //Add child to the last 329 dirStack~= dirStack[$-1].addChild(pathPart); 330 currStack++; 331 } 332 else if(dirStack[currStack].data != pathPart) //If both they are the same, check for the next part 333 { 334 dirStack = dirStack[0..$-1]; 335 currStack--; 336 } 337 else if(dirStack[currStack].data == pathPart) //If both they are the same, check for the next part 338 currStack++; 339 } 340 } 341 return root; 342 } 343 string buildPath(Node!string node) 344 { 345 string ret = node.data; 346 while(node.parent !is null) 347 { 348 node = node.parent; 349 if(node) 350 { 351 ret = node.data~"/"~ret; 352 } 353 } 354 return ret; 355 } 356 357 358 ///Copied from dmd. 359 unittest 360 { 361 assert(baseName("a/b/test.txt") == "test.txt"); 362 assert(relativePath("foo", "") == "foo"); 363 assert(filenameNoExt("helloWorld.zip") == "helloWorld"); 364 assert("/hello/test/again".isRootOf("/hello/test/again/something/is/here.txt")); 365 366 version (Posix) 367 { 368 assert(filename("/something/here/yet.txt"), "yet.txt"); 369 assert(filenameNoExt("/something/here/yet.txt"), "yet"); 370 371 assert(relativePath("foo", "/bar") == "foo"); 372 assert(relativePath("/foo/bar", "/foo/bar") == "."); 373 assert(relativePath("/foo/bar", "/foo/baz") == "../bar"); 374 assert(relativePath("/foo/bar/baz", "/foo/woo/wee") == "../../bar/baz"); 375 assert(relativePath("/foo/bar/baz", "/foo/bar") == "baz"); 376 } 377 version (Windows) 378 { 379 assert(filename(`c:\something\here\yet.txt`), "yet.txt"); 380 assert(filenameNoExt(`c:\something\here\yet.txt`) == "yet"); 381 382 assert(relativePath("foo", `c:\bar`) == "foo"); 383 assert(relativePath(`c:\foo\bar`, `c:\foo\bar`) == "."); 384 assert(relativePath(`c:\foo\bar`, `c:\foo\baz`) == `..\bar`); 385 assert(relativePath(`c:\foo\bar\baz`, `c:\foo\woo\wee`) == `..\..\bar\baz`); 386 assert(relativePath(`c:\foo\bar\baz`, `c:\foo\bar`) == "baz"); 387 assert(relativePath(`c:\foo\bar`, `d:\foo`) == `c:\foo\bar`); 388 } 389 }